package com.hzw.saas.service.storage.service;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Resource;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hzw.saas.api.storage.ICatalogService;
import com.hzw.saas.common.storage.enums.StorageModeEnum;
import com.hzw.saas.common.storage.service.IFileStorageService;
import com.hzw.saas.common.storage.service.IS3FileService;
import com.hzw.saas.common.util.enums.IsEnum;
import com.hzw.saas.common.util.exception.LostException;
import com.hzw.saas.service.storage.mapper.ResourceFileMapper;
import com.hzw.saas.service.storage.model.ResourceFile;
import com.hzw.saas.service.storage.pojo.FileStorageInfo;
import com.hzw.saas.service.storage.pojo.FileStorageStatus;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.PathUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * @author zzl
 * @since 10/22/2021
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class StorageModeHandler {

    private static final int DOWNLOAD_RETRY_TIMES = 3;
    private static final int UPLOAD_RETRY_TIMES = 3;

    // 存储服务
    private final ICatalogService catalogService;
    private final IFileStorageService fileStorageService;
    private final IS3FileService s3FileService;

    @Resource(name = ResourceFileServiceImpl.BEAN_NAME)
    private ServiceImpl<ResourceFileMapper, ResourceFile> fileService;

    protected void dealFileStorage(FileStorageInfo storage, StorageModeEnum storageMode) {
        if (storage == null) return;
        if (storageMode == null) {
            storageMode = StorageModeEnum.SAVE_IN_CLOUD;
        }

        FileStorageStatus storageStatus = this.dealStorage(storage, storageMode);
        this.updateByStorageStatus(storage, storageStatus, storageMode);
    }

    protected void dealFileStorage(List<FileStorageInfo> storageList, StorageModeEnum storageMode) {
        if (CollUtil.isEmpty(storageList)) return;
        if (storageMode == null) {
            storageMode = StorageModeEnum.SAVE_IN_CLOUD;
        }

        List<FileStorageStatus> storageStatusList = new ArrayList<>();
        StringBuilder errorMsg = new StringBuilder("");

        for (FileStorageInfo storage : storageList) {
            try {
                FileStorageStatus storageStatus = this.dealStorage(storage, storageMode);
                storageStatusList.add(storageStatus);
            } catch (Exception e) {
                if (errorMsg.length() == 0) {
                    errorMsg.append(e.getMessage());
                } else {
                    errorMsg.append(System.lineSeparator()).append(e.getMessage());
                }
            }
        }
        this.updateByStorageStatus(storageList, storageStatusList, storageMode);
        if (errorMsg.length() != 0) {
            throw new RuntimeException(errorMsg.toString());
        }
    }

    /**
     * 处理文件存储模式
     */
    private FileStorageStatus dealStorage(FileStorageInfo storage, StorageModeEnum storageMode) throws LostException {
        if (storage == null || StrUtil.hasBlank(storage.getCatalog(), storage.getResourceKey())) return null;

        String localFilePath = catalogService.getFilePath(storage.getCatalog(), storage.getResourceKey());
        File localFile = fileStorageService.getFile(localFilePath);

        FileStorageStatus storageStatus = null;
        // 处理转存
        log.debug("处理文件存储：{}", localFile);
        switch (storageMode) {
            case SAVE_IN_LOCAL:
                storageStatus = this.dealSaveInLocal(storage.getResourceKey(), localFile);
                break;
            case SAVE_IN_CLOUD:
                storageStatus = this.dealSaveInCloud(storage.getResourceKey(), localFile);
                break;
            case SAVE_IN_BOTH:
                storageStatus = this.dealSaveInBoth(storage.getResourceKey(), localFile);
                break;
            case AVAILABLE_IN_LOCAL:
                storageStatus = this.dealAvailableInLocal(storage.getResourceKey(), localFile);
                break;
            case AVAILABLE_IN_CLOUD:
                storageStatus = this.dealAvailableInCloud(storage.getResourceKey(), localFile);
                break;
            case CLEAR_FILE:
            case CLEAR_ALL:
                storageStatus = this.dealClearFile(storage.getResourceKey(), localFile);
                break;
        }
        return storageStatus;
    }

    private void updateByStorageStatus(FileStorageInfo storage, FileStorageStatus storageStatus, StorageModeEnum storageMode) {
        if (storage == null || storageStatus == null) return;

        if (Objects.equals(storageMode, StorageModeEnum.CLEAR_ALL)) {
            fileService.lambdaUpdate()
                .eq(ResourceFile::getResourceKey, storage.getResourceKey())
                .remove();
            return;
        }
        if (this.fileNeedUpdate(storage, storageStatus)) {
            fileService.lambdaUpdate().eq(ResourceFile::getResourceKey, storage.getResourceKey())
                .set(storageStatus.getIsLocal() != null, ResourceFile::getIsLocal, storageStatus.getIsLocal())
                .set(storageStatus.getIsCloud() != null, ResourceFile::getIsCloud, storageStatus.getIsCloud())
                .set(StrUtil.isNotBlank(storage.getCatalog()), ResourceFile::getCatalog, storage.getCatalog())
                .update();
        }
    }

    private void updateByStorageStatus(List<FileStorageInfo> storageList, List<FileStorageStatus> storageStatusList, StorageModeEnum storageMode) {
        if (CollUtil.isEmpty(storageList) || CollUtil.isEmpty(storageStatusList)) return;

        Map<String, FileStorageStatus> statusMap = storageStatusList.stream().collect(Collectors.toMap(
            FileStorageStatus::getResourceKey, s -> s, (k1, k2) -> k1));

        List<FileStorageInfo> storage2Update = storageList.stream()
            .filter(storage -> fileNeedUpdate(storage, statusMap.get(storage.getResourceKey())))
            .collect(Collectors.toList());

        if (CollUtil.isEmpty(storage2Update)) return;

        if (Objects.equals(storageMode, StorageModeEnum.CLEAR_ALL)) {
            List<String> key2UpdateList = storage2Update.stream()
                .map(FileStorageInfo::getResourceKey)
                .collect(Collectors.toList());
            fileService.lambdaUpdate()
                .in(ResourceFile::getResourceKey, key2UpdateList)
                .remove();
            return;
        }
        fileService.getBaseMapper().updateBatchFileStorageInfo(storage2Update);
    }

    private boolean fileNeedUpdate(FileStorageInfo storage, FileStorageStatus storageStatus) {
        if (storage == null || storageStatus == null) return false;

        return storageStatus.getIsLocal() != null && !Objects.equals(storageStatus.getIsLocal(), storage.getIsLocal())
            || storageStatus.getIsCloud() != null && !Objects.equals(storageStatus.getIsCloud(), storage.getIsCloud());
    }

    protected FileStorageStatus dealSaveInLocal(String resourceKey, File localFile) throws LostException {
        FileStorageStatus result = new FileStorageStatus(resourceKey, IsEnum.TRUE.getCode(), IsEnum.FALSE.getCode());
        // 本地已存在
        if (FileUtil.isFile(localFile)) {
            if (s3FileService.exists(resourceKey)) {
                s3FileService.deleteFile(resourceKey);
            }
            return result;
        }
        if (!s3FileService.exists(resourceKey)) {
            String errorMsg = StrUtil.format("{key: {}, localPath: {}, errorMsg: 云端文件丢失}!", resourceKey, localFile);
            throw new LostException(errorMsg);
        }
        // 从S3下载，同时删除S3
        this.downloadFromS3(resourceKey, localFile);
        if (FileUtil.isFile(localFile)) {
            s3FileService.deleteFile(resourceKey);
        }
        return result;
    }

    protected FileStorageStatus dealSaveInCloud(String resourceKey, File localFile) throws LostException {
        FileStorageStatus result = new FileStorageStatus(resourceKey, IsEnum.FALSE.getCode(), IsEnum.TRUE.getCode());
        // 云端已存在
        if (s3FileService.exists(resourceKey)) {
            if (FileUtil.isFile(localFile)) {
                FileUtil.del(localFile);
            }
            return result;
        }
        // 本地没有
        if (!FileUtil.isFile(localFile)) {
            String errorMsg = StrUtil.format("{key: {}, localPath: {}, errorMsg: 本地文件丢失}!", resourceKey, localFile);
            throw new LostException(errorMsg);
        }
        // 本地上传S3，同时删除本地
        this.uploadToS3(resourceKey, localFile);
        // 删除本地
        if (s3FileService.exists(resourceKey)) {
            FileUtil.del(localFile);
        }
        return result;
    }

    protected FileStorageStatus dealSaveInBoth(String resourceKey, File localFile) throws LostException {
        FileStorageStatus result = new FileStorageStatus(resourceKey, IsEnum.TRUE.getCode(), IsEnum.TRUE.getCode());
        // 云端与本地已存在
        boolean s3Exists = s3FileService.exists(resourceKey);
        boolean localExists = FileUtil.isFile(localFile);
        if (s3Exists && localExists) {
            return result;
        }
        if (s3Exists) {
            this.downloadFromS3(resourceKey, localFile);
            return result;
        }
        if (localExists) {
            this.uploadToS3(resourceKey, localFile);
            return result;
        }
        String errorMsg = StrUtil.format("{key: {}, localPath: {}, errorMsg: 文件丢失}!", resourceKey, localFile);
        throw new LostException(errorMsg);
    }

    protected FileStorageStatus dealAvailableInLocal(String resourceKey, File localFile) throws LostException {
        // 本地已存在
        if (FileUtil.isFile(localFile)) {
            return new FileStorageStatus(resourceKey, IsEnum.TRUE.getCode(), null);
        }
        if (!s3FileService.exists(resourceKey)) {
            String errorMsg = StrUtil.format("云端文件数据丢失(key: {}, localPath: {})!", resourceKey, localFile);
            throw new LostException(errorMsg);
        }
        // 从S3下载
        this.downloadFromS3(resourceKey, localFile);
        return new FileStorageStatus(resourceKey, IsEnum.TRUE.getCode(), IsEnum.TRUE.getCode());
    }

    protected FileStorageStatus dealAvailableInCloud(String resourceKey, File localFile) throws LostException {
        // 云端已存在
        if (s3FileService.exists(resourceKey)) {
            return new FileStorageStatus(resourceKey, null, IsEnum.TRUE.getCode());
        }
        // 本地没有
        if (!FileUtil.isFile(localFile)) {
            String errorMsg = StrUtil.format("本地文件数据丢失(key: {}, localPath: {})!", resourceKey, localFile);
            throw new LostException(errorMsg);
        }
        // 本地上传S3
        this.uploadToS3(resourceKey, localFile);
        return new FileStorageStatus(resourceKey, IsEnum.TRUE.getCode(), IsEnum.TRUE.getCode());
    }

    /**
     * 清空本地和s3
     */
    protected FileStorageStatus dealClearFile(String resourceKey, File localFile) {
        try {
            if (FileUtil.isFile(localFile)) {
                log.info("清理本地文件数据(key: {}, path: {})...", resourceKey, localFile);
                PathUtil.del(localFile.toPath());
            }
            if (s3FileService.exists(resourceKey)) {
                log.info("清理S3文件数据(key: {}, path: {})...", resourceKey, localFile);
                s3FileService.deleteFile(resourceKey);
            }
            return new FileStorageStatus(resourceKey, IsEnum.FALSE.getCode(), IsEnum.FALSE.getCode());
        } catch (Exception e) {
            String errorMsg = StrUtil.format("{key: {}, localPath: {}, errorMsg: 清理文件失败}!", resourceKey, localFile);
            throw new RuntimeException(errorMsg, e);
        }
    }

    protected void downloadFromS3(String resourceKey, File localFile) {
        int retryTimes = 1;
        while (true) {
            try {
                log.debug("从S3下载文件({key: {}, path: {}})，尝试次数: {}...", resourceKey, localFile, retryTimes);
                s3FileService.downloadFile(resourceKey, localFile.getAbsolutePath());
                return;
            } catch (Exception e) {
                if (retryTimes++ <= DOWNLOAD_RETRY_TIMES) {
                    continue;
                }
                String errorMsg = StrUtil.format("{key: {}, localPath: {}, errorMsg: S3下载文件失败}!", resourceKey, localFile);
                throw new RuntimeException(errorMsg, e);
            }
        }
    }

    protected void uploadToS3(String resourceKey, File localFile) {
        int retryTimes = 1;
        while (true) {
            try {
                log.debug("向S3上传文件({key: {}, path: {}}), 尝试次数: {}...", resourceKey, localFile, retryTimes);
                s3FileService.uploadFile(localFile, resourceKey);
                return;
            } catch (Exception e) {
                if (retryTimes++ <= UPLOAD_RETRY_TIMES) {
                    continue;
                }
                String errorMsg = StrUtil.format("{key: {}, localPath: {}, errorMsg: S3上传文件失败}!", resourceKey, localFile);
                throw new RuntimeException(errorMsg, e);
            }
        }
    }

}
